/*
 * Created on January 2, 2005
 *
 * Copyright (c) Patrick Armstrong 2002-2007. All Rights Reserved
 */
package org.dime.mine.game;

import java.awt.Point;
import java.math.BigInteger;
import java.security.InvalidParameterException;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Random;
import java.util.TreeMap;

/**
 * BasicBoard // TODO Add type summary
 * 
 * @author Owner
 */
public class BasicBoard implements Board
{
    private int mines;
    private int rows, columns;
    private BasicLocation[][] board;
    private int nonMines;
    private int marks = 0;
    private boolean won = false, lost = false;
    private BigInteger id;

    private final List<GameOutcomeListener> gameListeners = new LinkedList<GameOutcomeListener>();
    private final List<BoardListener> boardListeners = new LinkedList<BoardListener>();

    public BasicBoard()
    {
        this(16, 30, 99);
    }

    public BasicBoard(int rows, int columns, int mines)
    {
        if (rows <= 0 || columns <= 0)
            throw new InvalidParameterException(
                    "Board must have at least one row and at least one column");
        if (mines < 0 || mines > rows * columns)
            throw new InvalidParameterException(
                    "Board must have between zero and rows * columns mines, inclusive");

        int[][] tBoard = new int[rows][columns];
        tBoard = generateMines(tBoard, mines);
        tBoard = generateIndicators(tBoard);
        initialize(tBoard, mines);
    }

    protected final void initialize(int[][] board, int mines)
    {
        initializeBoard(board, mines);
    }

    private final void initializeBoard(int[][] board, int mines)
    {
        this.rows = board.length;
        this.columns = board[0].length;
        this.mines = mines;
        this.nonMines = columns * rows - this.mines;
        this.marks = 0;
        this.board = new BasicLocation[this.rows][this.columns];

        for (int i = 0; i < this.rows; i++)
        {
            for (int j = 0; j < this.columns; j++)
            {
                this.board[i][j] = new BasicLocation(i, j, board[i][j]);
            }
        }
    }

    public void addBoardListener(BoardListener l)
    {
        boardListeners.add(l);
    }

    public void removeBoardListener(BoardListener l)
    {
        boardListeners.remove(l);
    }

    public void addGameListener(GameOutcomeListener l)
    {
        gameListeners.add(l);
    }

    public void removeGameListener(GameOutcomeListener l)
    {
        gameListeners.remove(l);
    }

    public void open(int row, int column)
    {
        getLocation(row, column).open();
    }

    public void setMarked(int row, int column, boolean marked)
    {
        getLocation(row, column).setMarked(marked);
    }

    public int check(int row, int column)
    {
        return getLocation(row, column).getValue();
    }

    public boolean isGameOver()
    {
        return won || lost;
    }

    public boolean isWon()
    {
        return won;
    }

    public boolean isLost()
    {
        return lost;
    }

    public int getMines()
    {
        return mines;
    }

    public int getMarks()
    {
        return marks;
    }

    public int getRows()
    {
        return rows;
    }

    public int getColumns()
    {
        return columns;
    }

    public BigInteger getID()
    {
        return id;
    }

    public Board.Location getLocation(int row, int column)
    {
        return board[row][column];
    }

    @Override
    public String toString()
    {
        StringBuffer str = new StringBuffer();
        for (int i = 0; i < rows; i++)
        {
            for (int j = 0; j < columns; j++)
            {
                str.append(getLocation(i, j));
            }
            str.append("\n");
        }
        return str.toString();
    }

    /*
     * (non-Javadoc)
     * 
     * @see java.lang.Object#equals(java.lang.Object)
     */
    @Override
    public boolean equals(Object obj)
    {
        /*
         * Two boards are equal if and only if, for each mined location l1 in
         * board 1 there exists a location l2 in board 2 such that l1.getRow() ==
         * l2.getRow() and l1.getColumn() == l2.getColumn() and l2 is mined.
         * However there is no way to determine whether a given location is
         * mined without opening it. As a consequence it is only possible to
         * determine that two boards are equal when they are the exact same
         * object.
         */
        return super.equals(obj);
    }

    /*
     * (non-Javadoc)
     * 
     * @see java.lang.Object#hashCode()
     */
    @Override
    public int hashCode()
    {
        return super.hashCode();
    }

    protected int[][] generateMines(int[][] board, int mines)
    {
        Map<Integer, Point> m = new TreeMap<Integer, Point>();
        Random r = new Random();
        for (int i = 0; i < board.length; i++)
        {
            for (int j = 0; j < board[i].length; j++)
            {
                m.put(new Integer(r.nextInt()), new Point(i, j));
            }
        }

        Iterator<Integer> it = m.keySet().iterator();
        Point p;
        for (int i = 0; i < mines && it.hasNext(); i++)
        {
            p = m.get(it.next());
            board[p.x][p.y] = Board.MINE;
        }

        return board;
    }

    protected final int[][] generateIndicators(int[][] boardLayout)
    {
        int rows = boardLayout.length;
        int columns;
        for (int i = 0; i < boardLayout.length; i++)
        {
            columns = boardLayout[i].length;
            for (int j = 0; j < boardLayout[i].length; j++)
            {
                if (boardLayout[i][j] == Board.MINE)
                {
                    for (int k = i - 1; k <= i + 1; k++)
                    {
                        for (int l = j - 1; l <= j + 1; l++)
                        {
                            if (k >= 0 && k < rows && l >= 0 && l < columns
                                    && !(k == i && l == j)
                                    && boardLayout[k][l] != Board.MINE)
                                boardLayout[k][l]++;
                        }
                    }
                }
            }
        }

        return boardLayout;
    }

    protected void gameOver(boolean won)
    {
        fireGameOver();
        if (won)
        {
            this.won = true;
            fireGameWon();
        }
        else
        {
            this.lost = true;
            fireGameLost();
        }
    }

    protected boolean isValidLocation(int row, int column)
    {
        return BoardUtil.isValidLocation(this, row, column);
    }

    protected void fireGameOver()
    {
        for (Iterator<GameOutcomeListener> it = gameListeners.iterator(); it
                .hasNext();)
        {
            it.next().gameOver();
        }
    }

    protected void fireGameWon()
    {
        for (Iterator<GameOutcomeListener> it = gameListeners.iterator(); it
                .hasNext();)
        {
            it.next().gameWon();
        }
    }

    protected void fireGameLost()
    {
        for (Iterator<GameOutcomeListener> it = gameListeners.iterator(); it
                .hasNext();)
        {
            it.next().gameLost();
        }
    }

    protected void fireLocationMarked(BoardEvent be)
    {
        for (Iterator<BoardListener> it = boardListeners.iterator(); it
                .hasNext();)
        {
            it.next().locationMarked(be);
        }
    }

    protected void fireLocationOpened(BoardEvent be)
    {
        for (Iterator<BoardListener> it = boardListeners.iterator(); it
                .hasNext();)
        {
            it.next().locationOpened(be);
        }
    }

    protected void fireLocationUnmarked(BoardEvent be)
    {
        for (Iterator<BoardListener> it = boardListeners.iterator(); it
                .hasNext();)
        {
            it.next().locationUnmarked(be);
        }
    }

    private BoardEvent makeBoardEvent(Board.Location l)
    {
        return new BoardEvent(this, l);
    }

    protected class BasicLocation implements Board.Location
    {
        private final int row;
        private final int column;
        /**
         * The real value of the Location, what the user sees when the
         * Location's state is set to open
         */
        private final int value;

        private boolean open = false;
        private boolean marked = false;
        private int borderMarks = 0;

        protected BasicLocation(int row, int column, int value)
        {
            this.row = row;
            this.column = column;
            this.value = value;
        }

        public Board getBoard()
        {
            return BasicBoard.this;
        }

        public int getRow()
        {
            return row;
        }

        public int getColumn()
        {
            return column;
        }

        public int getValue()
        {
            if (isOpen())
                return value;
            else if (isMarked())
                return MARKED;
            else
                return NOT_OPEN;
        }

        public boolean isOpen()
        {
            return open;
        }

        public void open()
        {
            if (!isGameOver() && !isMarked() && !isOpen())
            {
                open = true;

                fireLocationOpened(makeBoardEvent(this));

                if (isMine())
                    gameOver(false);
                else
                {
                    nonMines--;
                    if (nonMines == 0)
                        gameOver(true);
                }
            }
        }

        public boolean isMarked()
        {
            return marked;
        }

        public void setMarked(boolean marked)
        {
            if (!isGameOver() && !isOpen())
            {
                if (marked && !this.marked)
                {
                    marks++;
                    this.marked = marked;
                    updateBorder(this.marked);
                    fireLocationMarked(makeBoardEvent(this));
                }
                else if (!marked && this.marked)
                {
                    marks--;
                    this.marked = marked;
                    updateBorder(this.marked);
                    fireLocationUnmarked(makeBoardEvent(this));
                }
            }
        }

        public boolean isSatisfied()
        {
            return isOpen() && borderMarks == value;
        }

        /*
         * (non-Javadoc)
         * 
         * @see java.lang.Object#toString(java.lang.Object)
         */
        @Override
        public String toString()
        {
            int i = getValue();
            String c;
            switch (i)
            {
                case MARKED:
                    c = "*";
                    break;
                case NOT_OPEN:
                    c = "#";
                    break;
                case 0:
                    c = " ";
                    break;
                default:
                    c = "" + i;
            }
            return c;
        }

        /*
         * (non-Javadoc)
         * 
         * @see java.lang.Object#equals(java.lang.Object)
         */
        @Override
        public boolean equals(Object obj)
        {
            if (obj instanceof Location)
            {
                Location loc = (Location) obj;
                return loc.getBoard().equals(this.getBoard())
                        && loc.getRow() == this.getRow()
                        && loc.getColumn() == this.getColumn();
            }
            return false;
        }

        /*
         * (non-Javadoc)
         * 
         * @see java.lang.Object#hashCode()
         */
        @Override
        public int hashCode()
        {
            return super.hashCode();
        }

        protected boolean isMine()
        {
            return value == MINE;
        }

        protected int getBorderMarks()
        {
            return borderMarks;
        }

        protected int incrementBorderMarks()
        {
            return ++borderMarks;
        }

        protected int decrementBorderMarks()
        {
            return --borderMarks;
        }

        private void updateBorder(boolean isMarked)
        {
            for (int i = row - 1; i <= row + 1; i++)
            {
                for (int j = column - 1; j <= column + 1; j++)
                {
                    if (isValidLocation(i, j) && !(i == row && j == column))
                    {
                        if (isMarked)
                            board[i][j].incrementBorderMarks();
                        else
                            board[i][j].decrementBorderMarks();
                    }
                }
            }
        }
    }
}